package com.black.utils.office.excel;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.black.utils.commons.display.InfoFrame;
import com.black.utils.commons.display.InfoPanel;
import com.black.utils.file.FileTypeEnum;
import com.opencsv.CSVWriter;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.springframework.util.CollectionUtils;

import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * @author ylx
 */
public class ExcelExportUtil {

    public static final int MAX_COLUMN_WIDTH = 255 * 256;

    /**
     * 根据文件名导出数据为Excel文件。支持XLS和XLSX两种格式。
     *
     * @param headers     Excel表格中的头部信息列表，定义了表格的列名和类型。
     * @param data        需要导出的数据列表，数据类型为泛型T，可以是任何类型。
     * @param fileName    导出的文件名，包括文件名和扩展名。扩展名用于确定导出的文件格式。
     * @param beforeWrite 导出Excel文件前的回调函数，可以在此函数中修改Workbook对象。
     *                    CSV HTML 不支持
     * @throws RuntimeException 如果文件扩展名不是XLS或XLSX，则抛出运行时异常。
     */
    public static void export(List<ExcelHeader> headers, JSONArray data, String fileName, Consumer<Workbook> beforeWrite) {
        ExcelEntity entity = new ExcelEntity(headers, data);
        export(entity, fileName, beforeWrite);
    }

    public static void export(List<ExcelHeader> headers, JSONArray data, String fileName) {
        export(headers, data, fileName, null);
    }

    public static void export(ExcelEntity excelEntity, String fileName) {
        export(excelEntity, fileName, null);
    }

    public static void export(ExcelEntity excelEntity, String fileName, Consumer<Workbook> beforeWrite) {
        export(Collections.singletonList(excelEntity), fileName, beforeWrite);
    }

    public static void export(List<ExcelEntity> entities, String fileName) {
        export(entities, fileName, null);
    }

    /**
     * 根据文件名后缀导出多个工作表到不同的文件格式。
     * 支持的格式包括：XLS、XLSX、CSV、HTML。
     * 对于CSV和HTML格式，只支持导出单个工作表。
     *
     * @param entities    导出的数据实体列表，每个实体代表一个工作表。
     * @param fileName    导出的文件名，包括文件名后缀。
     * @param beforeWrite 导出Excel文件前的回调函数，可以在此函数中修改Workbook对象。
     *                    CSV HTML 不支持
     * @throws RuntimeException 如果文件格式不受支持或导出多个工作表到CSV或HTML格式，则抛出运行时异常。
     */
    public static void export(List<ExcelEntity> entities, String fileName, Consumer<Workbook> beforeWrite) {
        if (entities == null || entities.isEmpty()) {
            return;
        }
        // 从文件名确定导出的文件格式。
        FileTypeEnum type = FileTypeEnum.getType(fileName);
        // 根据文件后缀名选择导出方式。
        switch (type) {
            case XLS:
                // 导出为XLS格式的Excel文件。
                export(false, entities, fileName, beforeWrite);
                break;
            case XLSX:
                // 导出为XLSX格式的Excel文件。
                export(true, entities, fileName, beforeWrite);
                break;
            case CSV:
                // 导出为CSV格式。
                // CSV格式只支持单个工作表。
                if (entities.size() > 1) {
                    throw new RuntimeException("不支持导出多个sheet到CSV文件");
                }
                ExcelEntity entityCsv = entities.get(0);
                exportCsv(entityCsv.getHeaders(), entityCsv.getData(), fileName);
                break;
            case HTML:
                // 导出为HTML格式。
                // HTML格式只支持单个工作表。
                if (entities.size() > 1) {
                    throw new RuntimeException("不支持导出多个sheet到HTML文件");
                }
                ExcelEntity entityHtml = entities.get(0);
                exportHtml(entityHtml.getHeaders(), entityHtml.getData(), fileName);
                break;
            default:
                // 如果文件后缀名不被支持，则抛出异常。
                throw new RuntimeException("不支持的文件类型;Unsupported file type");
        }
        InfoFrame instance = InfoFrame.getInstance();
        instance.setVisible(true);
        InfoPanel.printInfo("Exported successfully");
        InfoPanel.printUrl(fileName);
        InfoPanel.printInfo("Click to view");
    }

    /**
     * 导出Excel文件。
     * 根据传入的标志决定是导出XLSX格式还是XLS格式的Excel文件。
     * 使用提供的Excel实体列表来创建并填充Excel表格。
     *
     * @param isXlsx        标志位，指示导出的Excel文件格式是XLSX（true）还是XLS（false）。
     * @param excelEntities Excel实体列表，包含要导出的数据和元数据。
     * @param beforeWrite   导出Excel文件前的回调函数，可以在此函数中修改Workbook对象。
     *                      CSV HTML 不支持
     * @param fileName      要导出的Excel文件的名称。
     */
    private static void export(boolean isXlsx, List<ExcelEntity> excelEntities, String fileName, Consumer<Workbook> beforeWrite) {
        try (Workbook workbook = WorkbookFactory.create(isXlsx);
             FileOutputStream out = new FileOutputStream(fileName)) {
            // 遍历Excel实体列表，为每个实体创建一个工作表，并填充数据。
            for (int i = 0; i < excelEntities.size(); i++) {
                // 根据实体创建一个新的工作表。
                ExcelEntity excelEntity = excelEntities.get(i);
                String sheetName = excelEntity.getSheetName();
                sheetName = sheetName == null ? "Sheet" + i : sheetName;
                Sheet sheet = workbook.createSheet(sheetName);
                JSONArray data = excelEntity.getData();
                List<ExcelHeader> headers = excelEntity.getHeaders();
                // 计算头部的最大嵌套深度，用于后续创建Excel表格时确定列的宽度。
                int maxDepth = calculateDepth(headers);
                // 创建表头的样式，用于美化表头单元格。
                CellStyle headerStyle = createHeaderStyle(workbook);
                // 计算表头的宽度和高度，并设置表头的样式
                calculateHeader(headers, 0, maxDepth, maxDepth);
                // 根据头部信息和样式创建表格的头部部分。
                createHeader(headers, sheet, headerStyle, maxDepth);
                // 创建表格主体部分的样式
                CellStyle bodyStyle = createBodyStyle(workbook);
                // 将数据填充到表格中
                List<ExcelHeader> leafHeaders = getLeafHeaders(headers);
                addData(leafHeaders, data, sheet, bodyStyle, maxDepth);
                // 添加合并单元格
                List<int[]> mergeCells = excelEntity.getMergeCells();
                for (int[] mergeCell : mergeCells) {
                    addMergedRegion(sheet, mergeCell[0], mergeCell[1], mergeCell[2], mergeCell[3], bodyStyle);
                }
            }
            // 执行自定义的写入前操作
            if (beforeWrite != null) {
                beforeWrite.accept(workbook);
            }
            // 将工作簿写入到文件输出流中，完成Excel文件的导出。
            workbook.write(out);
        } catch (IOException e) {
            // 抛出运行时异常，以便调用者可以处理IO异常。
            throw new RuntimeException(e);
        }
    }

    /**
     * 将数据导出为CSV文件。
     * 此方法用于将给定的头部信息和数据数组转换为CSV格式，并写入指定的文件中。
     * CSV文件只支持单级表头，因此多级表头会导致异常。
     *
     * @param headers  表头信息列表，包含每个字段的名称和键。
     * @param data     数据数组，包含要导出的行数据。
     * @param fileName 导出的CSV文件名。
     * @throws RuntimeException 如果表头深度大于1或文件写入发生IO异常，则抛出运行时异常。
     */
    private static void exportCsv(List<ExcelHeader> headers, JSONArray data, String fileName) {
        // 计算表头的深度，CSV只支持单级表头
        int maxDepth = calculateDepth(headers);
        if (maxDepth > 1) {
            throw new RuntimeException("CSV 只支持单级表头; CSV only supports single-level headers");
        }

        try (CSVWriter writer = new CSVWriter(new FileWriter(fileName))) {
            // 将表头名称转换为字符串数组并写入CSV文件
            String[] headerNames = headers.stream().map(ExcelHeader::getName).toArray(String[]::new);
            writer.writeNext(headerNames);
            // 遍历数据数组，将每行数据写入CSV文件
            for (int i = 0; i < data.size(); i++) {
                JSONObject row = data.getJSONObject(i);
                String[] record = new String[headers.size()];
                // 遍历表头，根据键获取对应的数据值，并填充到行数据数组中
                for (int j = 0; j < headers.size(); j++) {
                    ExcelHeader header = headers.get(j);
                    String key = header.getKey();
                    String value = row.getString(key);
                    record[j] = value;
                }
                writer.writeNext(record);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 将给定的数据导出为HTML表格文件。
     * 此方法生成一个包含表格的HTML文件，表格的列标题由ExcelHeader对象定义，数据填充来自JSONArray。
     *
     * @param headers  列标题的列表，定义了表格的结构。
     * @param data     包含导出数据的JSONArray。
     * @param fileName 导出的HTML文件名。
     * @throws RuntimeException 如果文件写入过程中发生IOException。
     */
    private static void exportHtml(List<ExcelHeader> headers, JSONArray data, String fileName) {
        try (FileWriter writer = new FileWriter(fileName)) {
            // 初始化HTML文件结构和样式
            // 写入HTML文件的头部
            writer.write("<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>Exported Data</title>\n");
            writer.write("<style>\n");
            writer.write("table {border-collapse: collapse; width: 100%;}\n");
            writer.write("th, td {border: 1px solid black; padding: 8px; text-align: left;}\n");
            writer.write("th {background-color: #f2f2f2;}\n");
            writer.write("</style>\n");
            writer.write("</head>\n<body>\n");
            writer.write("<table>\n");

            // 处理表头
            // 计算最大深度，用于多级表头的处理
            int maxDepth = calculateDepth(headers);
            // 通过递归处理，构建多级表头的结构
            calculateHeader(headers, 0, maxDepth, maxDepth);
            // 将表头写入HTML文件
            createHeader(headers, writer, maxDepth);
            // 处理数据行
            // 获取最末尾的列标题，用于数据的正确映射和展示
            List<ExcelHeader> leafHeaders = getLeafHeaders(headers);
            // 将数据填充到HTML表格中
            addData(leafHeaders, data, writer);
            // 关闭HTML表格和文件
            // 关闭HTML标签
            writer.write("</table>\n</body>\n</html>");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 创建Excel表格的头部。根据提供的头部配置，在Excel表格中设置相应的标题单元格。
     * 同时处理了单元格的合并情况。
     *
     * @param headers     表格头部配置列表，包含了每个标题单元格的位置、大小和内容。
     * @param sheet       待操作的Excel表格的sheet对象。
     * @param headerStyle 标题单元格的样式。
     * @param maxDepth    表格头部的最大深度，用于处理多级表头宽度。
     */
    private static void createHeader(List<ExcelHeader> headers, Sheet sheet, CellStyle headerStyle, int maxDepth) {
        // 获取所有头部配置，包括嵌套的头部
        List<ExcelHeader> allHeaders = getAllHeaders(headers);
        // 遍历所有头部配置，进行单元格的创建和样式设置
        for (ExcelHeader header : allHeaders) {
            // 获取当前头部配置的行号、行合并数、列号、列合并数
            int rowNum = header.getRow();
            int rowspan = header.getRowspan();
            int column = header.getColumn();
            int colspan = header.getColspan();
            // 如果当前头部需要合并单元格，则添加合并区域到sheet中
            if (rowspan > 1 || colspan > 1) {
                // 如果当前头部的行合并或列合并范围大于1，则合并单元格
                addMergedRegion(sheet, rowNum, rowNum + rowspan - 1, column, column + colspan - 1, headerStyle);
            }
            // 获取或创建指定行号的行对象
            Row row = getRow(sheet, rowNum);
            // 获取或创建指定行号和列号的单元格对象
            Cell cell = getCell(row, column);
            // 设置单元格样式和内容
            cell.setCellStyle(headerStyle);
            cell.setCellValue(header.getName());
            if (header.getDepth() == maxDepth) {
                sheet.setColumnWidth(column, header.getColumnWidth());
            }
        }
    }

    /**
     * 创建Excel表格的头部HTML。
     *
     * @param headers  Excel头部信息的列表。
     * @param writer   用于写入HTML的FileWriter对象。
     * @param maxDepth 头部的最大深度，用于确定创建多少行HTML表格行。
     * @throws RuntimeException 如果写入过程中发生IOException。
     */
    private static void createHeader(List<ExcelHeader> headers, FileWriter writer, int maxDepth) {
        // 获取所有头部信息，包括嵌套的头部
        List<ExcelHeader> allHeaders = getAllHeaders(headers);
        // 根据行号对头部信息进行分组，以便按行处理
        Map<Integer, List<ExcelHeader>> rowHeaders = allHeaders.stream().collect(Collectors.groupingBy(ExcelHeader::getRow));
        try {
            // 遍历最大深度，为每一行写入HTML头部单元格
            for (int i = 0; i < maxDepth; i++) {
                // 开始一行
                writer.write("<tr>\n");
                // 获取当前行的头部信息，并按列号排序
                List<ExcelHeader> row = rowHeaders.get(i).stream().sorted(Comparator.comparingInt(ExcelHeader::getColumn)).collect(Collectors.toList());
                // 遍历当前行的每个头部信息，写入HTML单元格
                for (ExcelHeader header : row) {
                    // 根据头部的行跨度和列跨度写入HTML单元格
                    writer.write("<th rowspan=\"" + header.getRowspan() + "\" colspan=\"" + header.getColspan() + "\">" + header.getName() + "</th>\n");
                }
                // 结束一行
                writer.write("</tr>\n");
            }
        } catch (IOException e) {
            // 抛出运行时异常，以便调用者可以处理文件写入错误
            throw new RuntimeException(e);
        }
    }

    /**
     * 计算Excel表格头部的行跨度。
     * 计算Excel表格头部的行号,列号。
     * 该方法通过递归处理头部列表，为每个头部设置行跨度和列跨度，并根据头部的深度确定其在表格中的位置。
     *
     * @param headers      表格头部列表，包含所有级别的头部。
     * @param column       当前处理的列索引，用于确定头部的列号。
     * @param maxDepth     表格头部的最大深度，用于计算行数。
     * @param currentDepth 当前处理的头部的深度，用于计算行跨度。
     */
    private static void calculateHeader(List<ExcelHeader> headers, int column, int maxDepth, int currentDepth) {
        // 计算当前头部应该占据的行数
        int rowNum = maxDepth - currentDepth;
        for (ExcelHeader header : headers) {
            // 获取当前头部的深度
            int headerDepth = header.getDepth();
            // 计算当前头部的行跨度，包括当前行及下面的空行
            int rowSpan = (maxDepth - headerDepth) - (maxDepth - currentDepth) + 1;
            header.setRowspan(rowSpan);
            // 如果当前头部有子头部，递归处理子头部
            if (headerDepth > 1) {
                calculateHeader(header.getChildren(), column, maxDepth, headerDepth - 1);
            }
            // 设置当前头部的列索引和行索引
            // 更新当前列索引，以处理下一个头部
            header.setColumn(column);
            header.setRow(rowNum);
            // 根据当前头部的列跨度，更新下一头部的起始列索引
            column += header.getColspan();
        }
    }

    /**
     * 获取所有叶子节点的Excel头部信息。
     * <p>
     * 该方法递归遍历头部信息列表，寻找没有子节点的头部信息，即叶子节点。叶子节点代表了Excel表格中的一级字段，
     * 而非叶子节点则代表了包含多个子字段的复杂字段。
     *
     * @param headers 头部信息列表，可能包含多个层级的头部信息。
     * @return 只包含叶子节点的头部信息列表。
     */
    private static List<ExcelHeader> getLeafHeaders(List<ExcelHeader> headers) {
        // 初始化一个列表，用于存储所有叶子节点的头部信息。
        List<ExcelHeader> leafHeaders = new ArrayList<>();
        // 遍历输入的头部信息列表。
        for (ExcelHeader header : headers) {
            // 判断当前头部信息是否为叶子节点，即是否没有子节点。
            if (CollectionUtils.isEmpty(header.getChildren())) {
                // 如果是叶子节点，则直接添加到结果列表中。
                leafHeaders.add(header);
            } else {
                // 如果不是叶子节点，则递归调用本方法，处理当前头部信息的子节点。
                // 并将获取到的叶子节点添加到结果列表中。
                leafHeaders.addAll(getLeafHeaders(header.getChildren()));
            }
        }
        // 返回包含所有叶子节点的头部信息列表。
        return leafHeaders;
    }

    /**
     * 获取所有Excel头部信息的递归方法。
     * 该方法通过递归遍历头部信息列表及其子项，以构建一个包含所有头部信息的列表。
     *
     * @param headers Excel头部信息列表，可能包含子头部信息。
     * @return 包含所有头部信息及其子头部信息的列表。
     */
    private static List<ExcelHeader> getAllHeaders(List<ExcelHeader> headers) {
        // 初始化一个列表，用于存储所有头部信息。
        List<ExcelHeader> allHeaders = new ArrayList<>();
        // 遍历输入的头部信息列表。
        for (ExcelHeader header : headers) {
            // 将当前头部信息添加到结果列表中。
            allHeaders.add(header);
            // 获取当前头部信息的子头部信息列表。
            List<ExcelHeader> children = header.getChildren();
            // 如果子头部信息列表不为空，则递归调用本方法以获取所有子头部信息。
            if (!CollectionUtils.isEmpty(children)) {
                // 将获取到的子头部信息添加到结果列表中。
                allHeaders.addAll(getAllHeaders(children));
            }
        }
        // 返回包含所有头部信息的结果列表。
        return allHeaders;
    }


    /**
     * 向Excel表格中添加数据。
     * 该方法负责将JSON数组中的数据写入到Excel的指定表格中，根据Excel头部配置来映射数据。
     *
     * @param leafHeaders Excel的叶子级头部配置，用于映射数据到表格的具体列。
     * @param data        要写入Excel的数据，以JSON数组形式表示。
     * @param sheet       待添加数据的Excel表格。
     * @param bodyStyle   数据单元格的样式。
     * @param rowNum      开始添加数据的行号，用于计算实际插入行的行号。
     */
    private static void addData(List<ExcelHeader> leafHeaders, JSONArray data, Sheet sheet, CellStyle bodyStyle, int rowNum) {
        // 如果数据为空，则直接返回，不进行后续操作。
        if (CollectionUtils.isEmpty(data)) {
            return;
        }
        // 遍历数据数组，每项代表一行数据。
        for (int i = 0; i < data.size(); i++) {
            JSONObject rowData = data.getJSONObject(i);
            // 遍历叶子级头部配置，每项代表一个需要填入数据的列。
            for (ExcelHeader leafHeader : leafHeaders) {
                String key = leafHeader.getKey();
                // 如果当前行数据包含头部对应的键，则进行值的写入。
                if (rowData.containsKey(key)) {
                    // 计算实际要插入数据的行号。
                    // 获取当前行的行号
                    int row = rowNum + i;
                    // 获取当前列的列号
                    int column = leafHeader.getColumn();
                    // 获取当前列对应的值。
                    String value = rowData.getString(key);
                    // 如果值为空，则跳过当前列的写入操作。
                    if (StringUtils.isBlank(value)) {
                        continue;
                    }
                    // 如果当前列的宽度小于值长度，则更新列的宽度。 实现宽度自适应
                    int columnWidth = sheet.getColumnWidth(column);
                    if (columnWidth < value.length() * 256) {
                        int width = Integer.min(MAX_COLUMN_WIDTH, value.length() * 256);
                        sheet.setColumnWidth(column, width);
                    }
                    // 获取目标单元格，并设置值及样式。
                    Cell cell = getCell(getRow(sheet, row), column);
                    cell.setCellValue(value);
                    cell.setCellStyle(bodyStyle);
                }
            }
        }
    }

    /**
     * 将数据添加到Excel文件中。此方法负责将数据行渲染为HTML表格的格式，并写入到文件中。
     *
     * @param leafHeaders 表头的叶子节点列表，用于确定表格的列。
     * @param data        数据集合，包含要写入表格的行数据。
     * @param writer      文件写入器，用于将渲染后的HTML写入到Excel文件中。
     * @throws IOException 如果写入文件过程中发生错误，则抛出此异常。
     */
    private static void addData(List<ExcelHeader> leafHeaders, JSONArray data, FileWriter writer) throws IOException {
        leafHeaders = leafHeaders.stream().sorted(Comparator.comparingInt(ExcelHeader::getColumn)).collect(Collectors.toList());
        // 遍历数据集合中的每一项，每一项代表表格的一行数据
        for (int i = 0; i < data.size(); i++) {
            JSONObject rowData = data.getJSONObject(i);
            // 开始写入一行数据
            writer.write("<tr>\n");
            // 遍历表头的叶子节点，对应表格的每一列
            for (ExcelHeader leafHeader : leafHeaders) {
                String key = leafHeader.getKey();
                // 获取当前列的数据值
                String value = rowData.getString(key);
                value = StringUtils.isBlank(value) ? "" : value;
                // 写入单元格数据，如果值为空，则显示为空字符串
                writer.write("<td>" + value + "</td>\n");
            }
            // 结束当前行的写入
            writer.write("</tr>\n");
        }
    }

    /**
     * 计算列跨度
     * 计算Excel表格头部的深度（即最大层级数）。
     * 该方法通过递归遍历Excel表格的头部结构，确定最深的层次。
     *
     * @param headers Excel表格的头部结构列表，头部结构代表了表格的一列或多列。
     * @return 返回头部结构的最大深度，即表格的最深层级。
     */
    // 计算深度的方法
    public static int calculateDepth(List<ExcelHeader> headers) {
        // 初始化最大深度为0
        int maxDepth = 0;
        // 遍历头部结构列表
        for (ExcelHeader header : headers) {
            // 递归计算当前头部结构的深度
            int depth = calculateDepth(header);
            // 设置当前头部结构的深度
            header.setDepth(depth);
            // 如果当前头部结构的深度大于已知的最大深度，则更新最大深度
            if (depth > maxDepth) {
                maxDepth = depth;
            }
        }
        // 返回计算出的最大深度
        return maxDepth;
    }

    /**
     * 计算列跨度
     * 计算Excel表格头部的深度（即最大层级数）。
     * 递归方法，通过遍历头部元素及其子元素来确定整个头部结构的深度。
     *
     * @param header 表格头部元素，代表当前正在处理的头部层级。
     * @return 当前头部元素及其子元素的深度。
     */
    private static int calculateDepth(ExcelHeader header) {
        // 如果当前头部没有子元素，则认为深度为1（即叶子）
        if (CollectionUtils.isEmpty(header.getChildren())) {
            if (StringUtils.isBlank(header.getKey())) {
                throw new RuntimeException("叶子结点的表头对应字段不能为空;The corresponding field of the leaf node header cannot be empty");
            }
            header.setDepth(1);
            header.setColspan(1);
            return 1;
        }

        int maxDepth = 0;
        // 遍历当前头部的所有子元素，计算它们的深度
        for (ExcelHeader child : header.getChildren()) {
            int childDepth = calculateDepth(child);
            child.setDepth(childDepth);
            // 更新最大深度值
            if (childDepth > maxDepth) {
                maxDepth = childDepth;
            }
        }

        // 计算当前头部元素的列跨度，基于其所有子元素的列跨度之和
        header.setColspan(header.getChildren().stream().mapToInt(ExcelHeader::getColspan).sum());
        // 返回当前头部元素及其子元素的深度之和加1
        return maxDepth + 1;
    }

    /**
     * 向工作表中添加合并的单元格区域。(若不预创建单元格, 样式会只应用到合并前的左上角单元格)
     * 此方法通过设置单元格样式和创建一个单元格范围地址来合并指定的单元格区域。
     *
     * @param sheet       要操作的工作表。
     * @param firstRow    合并区域的起始行索引。
     * @param lastRow     合并区域的结束行索引。
     * @param firstColumn 合并区域的起始列索引。
     * @param lastColumn  合并区域的结束列索引。
     * @param cellStyle   应用于合并区域的所有单元格的样式。
     */
    private static void addMergedRegion(Sheet sheet, int firstRow, int lastRow, int firstColumn, int lastColumn, CellStyle cellStyle) {
        // 遍历每一行，从firstRow到lastRow
        for (int i = firstRow; i <= lastRow; i++) {
            // 获取当前行
            Row row = getRow(sheet, i);
            // 遍历当前行的每一列，从firstColumn到lastColumn
            for (int j = firstColumn; j <= lastColumn; j++) {
                // 获取当前单元格
                Cell cell = getCell(row, j);
                // 设置当前单元格的样式
                cell.setCellStyle(cellStyle);
            }
        }
        // 创建一个单元格范围地址，并添加到工作表中，以合并指定的区域
        sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstColumn, lastColumn));
    }

    /**
     * 创建用于表格标题的单元格样式。
     * 样式包括字体设置（名称、大小、加粗）、水平和垂直对齐方式设置，以及边框样式设置。
     *
     * @param workbook Excel工作簿对象，用于创建单元格样式和字体。
     * @return 返回创建的标题单元格样式。
     */
    private static CellStyle createHeaderStyle(Workbook workbook) {
        // 创建一个新的单元格样式对象
        CellStyle headerStyle = workbook.createCellStyle();
        // 创建一个新的字体对象，并设置其属性
        Font font = workbook.createFont();
        font.setFontName("Arial"); // 设置字体名称为Arial
        font.setFontHeightInPoints((short) 14); // 设置字体大小为14点
        font.setBold(true); // 设置字体加粗
        // 将创建的字体应用到单元格样式
        headerStyle.setFont(font);
        // 设置单元格内容水平和垂直居中对齐
        headerStyle.setAlignment(HorizontalAlignment.CENTER);
        headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        // 设置单元格边框样式为细边框
        BorderStyle borderStyle = BorderStyle.THIN;
        headerStyle.setBorderTop(borderStyle); // 设置上边框样式
        headerStyle.setBorderBottom(borderStyle); // 设置下边框样式
        headerStyle.setBorderLeft(borderStyle); // 设置左边框样式
        headerStyle.setBorderRight(borderStyle); // 设置右边框样式

        // 返回创建的标题单元格样式
        return headerStyle;
    }

    /**
     * 创建数据单元格样式
     *
     * @param workbook 工作簿
     * @return 样式
     */
    private static CellStyle createBodyStyle(Workbook workbook) {
        CellStyle bodyStyle = workbook.createCellStyle();
        // 设置单元格内容水平和垂直居中对齐
        bodyStyle.setAlignment(HorizontalAlignment.LEFT);
        // 设置单元格边框样式为细边框
        BorderStyle borderStyle = BorderStyle.THIN;
        bodyStyle.setBorderTop(borderStyle); // 设置上边框样式
        bodyStyle.setBorderBottom(borderStyle); // 设置下边框样式
        bodyStyle.setBorderLeft(borderStyle); // 设置左边框样式
        bodyStyle.setBorderRight(borderStyle); // 设置右边框样式
        return bodyStyle;
    }

    /**
     * 获取行, 没有则创建
     *
     * @param sheet  工作表
     * @param rowNum 行号(from 0)
     * @return 指定行
     */
    public static Row getRow(Sheet sheet, int rowNum) {
        Row row = sheet.getRow(rowNum);
        if (row == null) {
            row = sheet.createRow(rowNum);
        }
        return row;
    }

    /**
     * 获取单元格, 没有则创建
     *
     * @param row    指定行
     * @param column 指定列(from 0)
     * @return 单元格
     */
    public static Cell getCell(Row row, int column) {
        Cell cell = row.getCell(column);
        if (cell == null) {
            cell = row.createCell(column);
        }
        return cell;
    }
}
